/**

	Simplistic spell checker by Rich Blockley 2013
	----------------------------------------------

	Assumptions:
		* Dictionary is one word per line (although not limited to this).
		* Dictionary can end with or without a line feed.
		* Input file can end with or without a line feed.
		* Words in the input file are separated by a set of characters, NON_WORD_CHARS (below), which is non-exhaustive.
		* Deliberately, hyphen (-) and quote (') are not word separators, as they are letters in words.
		* Brackets, double-quotes and everything else are treated as parts of words, and will be judged by the dictionary list.
	
	Implementation decisions:
		* Code in one file.
		* Built with Visual C++ 2010 Express and MinGW on Windows, and FreeBSD.
		* Case-insensitive spell checker.
		* Either input file can end with a line feed.
		* Sorted dictionary for faster loop up, using qsort. Words are looked up in the dictionary using bsearch.
		* The dictionary list concept of pointers to strings was deliberately chosen for use with qsort and bsearch.
		* Entire input and dictionary stored in memory while running (as opposed to streaming the input file, allowing for extra-large files).
		* Can cope with junk in the dictionary, the words will be found.
		* Can start in debug mode by passing a 3rd parameter, to have various runtime data written to stderr.
	
	Points to note:
		* No arbitrary bound on size of words.
		* Words delimited by NON_WORD_CHARS array of characters. This is a basic set.
		* Non-dictionary word repeatedly printed when repeatedly found (architecture easily allows once-per-word).
		* Portability by using stdint or stdbool where possible, using native C types for sizes, and only C standard library functions.
		* Line handling portability by knowing to break words at \r or \n boundaries.

*/

#include "spellcheck.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#ifdef _MSC_VER /* If Visual C++ 2010, define bool as stdbool.h was not working right */

typedef unsigned char bool;
#define true 1u
#define false 0u

#endif

#ifdef __GNUC__ /* GCC, use stdbool.h */
#include <stdbool.h>
#endif

static struct core coreDataObj;
static struct core* coreData = &coreDataObj;
static const char* NON_WORD_CHARS = " .;,!?\r\n";

bool processDictionary(const char*);
bool processCheck(const char* f);
void toLower(char*);
int specialStrCmp(const char** const, const char** const);
void cleanup(void);

/** Output an error to stderr */
void myErr(const char* e)
{
	fprintf(stderr, "*** %s\n", e);
}

/* Entry */
int main(int argc, const char* argv[])
{

	int rtn;

	memset(coreData, 0, sizeof(coreDataObj)); /* blank */

	if (argc < 3)
	{
		myErr("Required: spellchecker <dictionaryFile> <inputFile>");
		rtn = -1;
	}
	else
	{
		/* Got enough program parameters */

		bool dictOk;

		/* Got debug? */
		if (argc == 4)
		{
			coreData->debug = true;
		}

		/* Process dictionary into memory */
		dictOk = processDictionary(argv[1]);

		if (dictOk)
		{
			
			bool checkOk = processCheck(argv[2]);

			rtn = !checkOk;
		}
		else
		{
			rtn = -1; /* Errors would have been written by processDictionary() */
		}
	}

	/* All done */
	cleanup();

	return rtn;
}

/**
	Read in the dictionary, split into words, create a word list, and sort.
*/
bool processDictionary(const char* d)
{

	bool rtn;

	/* open dictionary */
	coreData->dictionaryFile = fopen(d, "rb");

	if (coreData->dictionaryFile == NULL)
	{
		myErr("Cannot open dictionary file.");
		rtn = false;
	}
	else
	{
		size_t wasRead;

		fseek(coreData->dictionaryFile, 0, SEEK_END);
		coreData->dictionaryFileSize = ftell(coreData->dictionaryFile);
		rewind(coreData->dictionaryFile);

		/* One extra, to store a NULL on the end of the buffer, so that strtok can correctly find an end */
		coreData->dictionaryBufferSize = coreData->dictionaryFileSize + 1;
		coreData->dictionaryBuffer = malloc(coreData->dictionaryBufferSize);

		wasRead = fread(coreData->dictionaryBuffer, 1, coreData->dictionaryFileSize, coreData->dictionaryFile);

		fclose(coreData->dictionaryFile);
		coreData->dictionaryFile = NULL;

		/* Set final byte to be NULL */
		((uint8_t*) coreData->dictionaryBuffer)[coreData->dictionaryBufferSize - 1] = 0;

		if (wasRead != coreData->dictionaryFileSize)
		{
			myErr("Could not read the exact amount of dictionary file as determined by file size.");
			rtn = false;
		}
		else
		{

			/*
				Create the dictionary string table:
					strtok through the dictionary, finding strings.
					Add them to the dictionaryItems list. Resize as necessary.
					Lowercase them as we go.
					Sort the list of pointers.

			*/
			bool done = false;
			bool first = true;

			coreData->dictionaryItemsSize = 1;
			coreData->dictionaryItems = (char**) malloc(sizeof(coreData->dictionaryItems[0]) * coreData->dictionaryItemsSize);

			while (!done)
			{
				char* item = NULL;

				/* First call to strtok? */
				if (first)
				{
					item = strtok((char*) coreData->dictionaryBuffer, NON_WORD_CHARS);
					first = false;
				}
				else
				{
					item = strtok(NULL, NON_WORD_CHARS);
				}

				/* No more items? */
				if (item == NULL)
				{
					/* End of loop */
					done = true;
				}
				else
				{
					toLower(item);

					/* No more space to hold dictionary item pointers? */
					if (coreData->dictionaryItemsUsed == coreData->dictionaryItemsSize)
					{
						/* Double it */
						coreData->dictionaryItemsSize *= 2;
						coreData->dictionaryItems = (char**) realloc(coreData->dictionaryItems, sizeof(coreData->dictionaryItems[0]) * coreData->dictionaryItemsSize);

					}

					/* Add item to list */
					coreData->dictionaryItems[coreData->dictionaryItemsUsed++] = item;
				}
			}

			/* Sort the pointer list using specialStrCmp */
			qsort(coreData->dictionaryItems, coreData->dictionaryItemsUsed, sizeof(coreData->dictionaryItems[0]), (int (*) (const void*, const void*)) specialStrCmp);

			rtn = true;
		}

	}


	/*
		A bit of debug output to stderr while developing.
	*/
	if (coreData->debug)
	{
		uint32_t i;

		fprintf(stderr, "NumDictItems: %d\n", coreData->dictionaryItemsUsed);

		for (i = 0; i < coreData->dictionaryItemsUsed; i++)
		{
			fprintf(stderr, "DictItem: %s\n", coreData->dictionaryItems[i]);
		}
	}


	
	return rtn;
}

/**
	In-place to-lower-case function.
*/
void toLower(char* s)
{
	while (*s != 0)
	{
		if (*s >= 'A' && *s <= 'Z') /* Current char is uppercase */
			*s += 32; /* To lowercase */
		s++;
	}
}

/**
	Compare function for use with qsort to sort two pointers to strings.
	Using strcoll as the locale-dependant string sorter.
*/
int specialStrCmp(const char** const left, const char** const right)
{
	return strcoll(*left, *right);
}

/**
	Spell-check the input file, in light of the loaded dictionary.
*/
bool processCheck(const char* f)
{

	bool rtn;

	/* open input file */
	coreData->inputFile = fopen(f, "rb");

	if (coreData->inputFile == NULL)
	{
		myErr("Cannot open input file.");
		rtn = false;
	}
	else
	{
		size_t wasRead;

		/* Obtain input file size */
		fseek(coreData->inputFile, 0, SEEK_END);
		coreData->inputFileSize = ftell(coreData->inputFile);
		rewind(coreData->inputFile);

		/* One extra, to store a NULL on the end of the buffer, so that strtok can correctly find an end */
		coreData->inputBufferSize = coreData->inputFileSize + 1;
		coreData->inputBuffer = malloc(coreData->inputBufferSize);

		wasRead = fread(coreData->inputBuffer, 1, coreData->inputFileSize, coreData->inputFile);

		fclose(coreData->inputFile);
		coreData->inputFile = NULL;

		/* Set final byte to be NULL */
		((uint8_t*) coreData->inputBuffer)[coreData->inputBufferSize - 1] = 0;

		if (wasRead != coreData->inputFileSize)
		{
			myErr("Could not read the exact amount of input file as determined by file size.");
			rtn = false;
		}
		else
		{

			bool first = true, done = false;
			char* item;
			char** const pointerToItem = &item; /* Const pointer to changeable data */
			char* itemInDict;

			/* The checking loop */
			while (!done)
			{
				/* Find a word */
				if (first)
				{
					item = strtok((char*) coreData->inputBuffer, NON_WORD_CHARS);
					first = false;
				}
				else
				{
					item = strtok(NULL, NON_WORD_CHARS);
				}

				/* If NULL returned then we're at the end of the input file */
				if (item == NULL)
				{
					done = true;
				}
				else
				{
					toLower(item); /* Lowercase this */

					/* Is item in dictionary? */
					itemInDict = (char*) bsearch((const void*) pointerToItem, coreData->dictionaryItems, coreData->dictionaryItemsUsed, sizeof(coreData->dictionaryItems[0]), (int (*) (const void*, const void*)) specialStrCmp);

					/* If not in dictionary, print word to stdout */
					if (itemInDict == NULL)
					{
						printf("%s\n", item);
					}
				}
			}

			rtn = 1;
		}
	}

	return rtn;
}

/**
	Free all allocated memory.
*/
void cleanup(void)
{
	free(coreData->dictionaryBuffer);
	free(coreData->inputBuffer);
	coreData->dictionaryBufferSize = 0;
	coreData->dictionaryItemsSize = 0;
}
